Skip to content

Annotate pure factory functions/calls with PURE / NO_SIDE_EFFECTS#21465

Draft
NullVoxPopuli-ai-agent wants to merge 2 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/pure-annotations
Draft

Annotate pure factory functions/calls with PURE / NO_SIDE_EFFECTS#21465
NullVoxPopuli-ai-agent wants to merge 2 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/pure-annotations

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor

Adds /* @__PURE__ */ and /* @__NO_SIDE_EFFECTS__ */ annotations to ember-source's side-effect-free factory functions and their module-scope call sites.

Rollup/rolldown can't prove these calls pure on their own — the callee almost always lives in another module — so a single module-scope Mixin.create(...), X.extend(...), createPrimitiveRef(...), new Cache(...), etc. is enough to drag the whole module into a consumer's graph. The annotation removes that anchor.

What's annotated

@__NO_SIDE_EFFECTS__ on pure factory definitions

  • @glimmer/reference: createPrimitiveRef + createConstRef / createUnboundRef / createComputeRef / createReadOnlyRef / createInvokableRef
  • internalHelper (glimmer + ember), intern (glimmer + ember), makeDictionary
  • templateFactory (@glimmer/opcode-compiler), template (@ember/template-compiler)

@__PURE__ on module-scope factory calls

  • Mixin.create(...) and FrameworkObject / CoreObject / EmberObject / Service / Namespace.extend(...) across the runtime/views mixins, @ember/array, @ember/controller, @ember/engine, @ember/enumerable, @ember/object, @ember/routing, and ember-testing
  • createPrimitiveRef('ember-view') in the curly component manager
  • intern(...) for GUID_KEY
  • the @ember/-internals/string and dasherize-component-name Caches
  • @glimmer/program's constants() / .indexOf() and DEFAULT_TEMPLATE's JSON.stringify

Safety

These are inert in ember-source's own builds — our rollup config keeps all module side effects (moduleSideEffects: true), so dist/dev and dist/prod are byte-for-byte equivalent in behavior. They only give downstream bundlers (and the side-effect detection in #21449) permission to drop the results when they're unused. Every annotated call is a pure value factory whose result is referenced whenever the module is actually used, so a consumer that keeps the binding keeps the call.

Effect

  • The tree-shakability snapshot gains three newly-shakable entrypoints: @ember/-internals/container, @ember/-internals/views/lib/compat/fallback-view-registry, @ember/-internals/views/lib/system/utils.
  • Combined with the side-effect detection in Add plugin that declares side-effects #21449, several modules drop off the generated sideEffects list (e.g. curly, @ember/-internals/string, @glimmer/program).

Relationship to other PRs

This supersedes the manual source annotations in #21449 and broadens them; #21449 keeps the side-effect-detection plugin and the generated sideEffects list. It composes with the renderer/classic-cleanup refactors (#21463 / #21464) — the remaining hello-world weight (classic Component, EmberObject, computed, …) is held in by reopen() / manager-registration patterns that those PRs address, not by anything annotations can reach.

🤖 Generated with Claude Code

NullVoxPopuli and others added 2 commits June 11, 2026 10:38
Adds `/* @__PURE__ */` and `/* @__NO_SIDE_EFFECTS__ */` annotations to
ember-source's side-effect-free factory functions and their module-scope
call sites. Rollup/rolldown cannot prove these calls pure on their own
(the callee usually lives in another module), so without the annotation
a single module-scope `Mixin.create(...)`, `X.extend(...)`,
`createPrimitiveRef(...)`, `new Cache(...)`, etc. keeps the whole module
in the consumer's graph.

Annotated, by kind:
- NO_SIDE_EFFECTS on pure factories: createPrimitiveRef + the rest of the
  reference constructors (createConstRef/createUnboundRef/createComputeRef/
  createReadOnlyRef/createInvokableRef), internalHelper (glimmer + ember),
  intern (glimmer + ember), makeDictionary, templateFactory, template.
- PURE on module-scope factory calls: Mixin.create(...) and
  FrameworkObject/CoreObject/EmberObject/Service/Namespace.extend(...)
  across the mixins, array, controller, engine, enumerable, object,
  routing, and ember-testing modules; createPrimitiveRef('ember-view')
  in curly; intern(...) for GUID_KEY; the @ember/-internals/string and
  dasherize-component-name Caches; @glimmer/program's constants()/indexOf
  and DEFAULT_TEMPLATE's JSON.stringify.

These are inert in ember-source's own bundles (its rollup keeps all
module side effects); they only let downstream bundlers — and the
side-effect detection in emberjs#21449 — drop the unused results. The
tree-shakability snapshot gains three newly-shakable entrypoints
(@ember/-internals/container, views/lib/compat/fallback-view-registry,
views/lib/system/utils).

Supersedes the manual annotations in emberjs#21449, which keeps the
side-effect-detection plugin and generated sideEffects list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`template()` from @ember/template-compiler calls setComponentTemplate(),
which is the whole point of the common statement-position usage:

    class Foo extends GlimmerishComponent {
      static {
        template('<button {{on "click" this.handleClick}}/>', { component: this });
      }
    }

Here the result is discarded, so /* @__NO_SIDE_EFFECTS__ */ let the
bundler delete the call entirely — the component never gets its template
and renders nothing. This broke the runtime template compiler suites and
the keyword "no eval and no scope" tests (Cannot read properties of null
(reading 'click')).

Drop the annotation; the function is genuinely effectful.
@NullVoxPopuli

Copy link
Copy Markdown
Contributor

📊 Size report

Tarball size1.2 MB1.2 MB

dist/dev   0.05%↑

File Before (Size / Brotli) After (Size / Brotli)
./packages/@ember/-internals/routing/index.js 599 B / 202 B 22%↑730 B / 16%↑234 B
./packages/@ember/routing/-internals.js 736 B / 249 B 20%↑883 B / 14%↑283 B
./packages/shared-chunks/api-{hash}.js 10 kB / 2.1 kB 147%↑24.8 kB / 156%↑5.4 kB
./packages/shared-chunks/reference-{hash}.js 4.9 kB / 1.3 kB 3%↑5.1 kB / 1%↑1.3 kB
./packages/shared-chunks/template-{hash}.js 491 B / 203 B 118%↑1.1 kB / 98%↑401 B
Total (Includes all files) 2.1 MB / 491.9 kB 0.05%↑2.1 MB / 0.1%↑492.4 kB

dist/prod   0.08%↑

File Before (Size / Brotli) After (Size / Brotli)
./packages/@ember/-internals/routing/index.js 568 B / 198 B 45%↑826 B / 35%↑268 B
./packages/@ember/application/index.js 31.9 kB / 8.2 kB 0.6%↑32.1 kB / 0.8%↑8.3 kB
./packages/@ember/object/core.js 26.1 kB / 6.4 kB -0.43%↓26 kB / -0.53%↓6.4 kB
./packages/@ember/routing/lib/utils.js 6.7 kB / 2 kB 4%↑6.9 kB / 3%↑2.1 kB
./packages/@ember/routing/route.js 54.1 kB / 12 kB 0.2%↑54.3 kB / 0.3%↑12 kB
./packages/@ember/routing/router.js 44.1 kB / 10.2 kB 0.4%↑44.3 kB / 0.6%↑10.3 kB
./packages/shared-chunks/reference-{hash}.js 4.3 kB / 1.2 kB 4%↑4.5 kB / 3%↑1.2 kB
Total (Includes all files) 1.9 MB / 450 kB 0.08%↑1.9 MB / 0.1%↑450.6 kB

smoke-tests/v2-app-hello-world-template/dist   -0.58%↓

File Before (Size / Brotli) After (Size / Brotli)
./assets/main-{hash}.js 158.7 kB / 43.6 kB -0.58%↓157.8 kB / -0.62%↓43.4 kB
Total (Includes all files) 159.1 kB / 43.8 kB -0.58%↓158.1 kB / -0.6%↓43.5 kB

🤖 This report was automatically generated by wyvox/pkg-size

@NullVoxPopuli NullVoxPopuli marked this pull request as draft June 11, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants